Browser : 브라우저의 CORS 정책, CORS 이전엔 어떻게 개발했나

SOP (Same-Origin-Policy)

브라우저는 기본적으로 동일 출처 정책을 따른다. 쿠키, 세션, 사용자 정보 같은 걸 외부 사이트에서 훔칠 수 있으니까 다른 출처(origin) 의 리소스를 마음대로 읽지 못하게 막는다.

다른 출처로의 요청(이미지 <img/>, 스크립트 <script>, 폼 <form>, 링크 <a>, iframe src 등)은 보낼 수 있다. 단, 그 응답 내용을 JS에서 읽는 것(예: fetch의 응답, XHR.responseText, iframe.contentWindow.document 등) 은 차단된다. 대신 script의 경우 실행까지는 된다.

하지만 웹 생태계가 커지면서, 교차 출처 요청에 대한 허용이 필요해졌고, 교차 출처를 허용하기 위한 정책이 CORS다.


JSONP : CORS 전에는 어떻게 했을까

CORS가 없던 시절, 개발자들이 이 제한을 회피하기 위해 썼던 꼼수가 바로 JSONP (JSON with Padding)

SOP에서, <script/>가 보내는 요청은 예외였다. 스크립트는 “실행”을 목적으로 하기 때문에, 다른 출처의 JS도 자유롭게 불러오고 실행까지 가능했다.

<script src="https://api.example.com/user?callback=handleData"></script>
<script>
  function handleData(data) {
    console.log(data.name); // 서버에서 내려준 JSONP 실행
  }
</script>

그럼 데이터를 JS 코드처럼 포장해서 <script>로 불러오는 방식으로 우회했다.

handleData({ name: '병건', age: 28 });

하지만, 이 방식은 한계가 분명했다.

  • Get 요청만 가능했고
  • <script />는 그냥 실행만 될 뿐, 응답 상태(404, 500 등)를 감지할 방법이 없어서 에러처리도 불가능했으며
  • 서버가 악성 JS를 내려주면 그대로 실행됨

CORS (Cross-Origin-Request-Policy)

웹 생태계가 커지면서, 교차 출처 요청에 대한 허용이 필요해졌고, 교차 출처가 가능하게 끔 만들어진 정책이 CORS다.

서버가 브라우저에게 ‘이 출처의 요청은 허용해도 돼’라고 명시적으로 알려주는 표준 방식으로
CORS 정책에 따라 교차출처 요청일 땐, 기본적으로 아래의 단계를 따른다.

  1. 브라우저는 요청할 때 자동으로 Origin 헤더를 붙이고,
  2. 서버가 Access-Control-Allow-Origin 으로 허용하면 통과시켜 줌.
  3. 그러면 리소스 읽을 수 있다.

Preflight 요청

교차 출처 요청을 허용하는 단계에는 Preflight 요청도 포함된다. 실제 요청을 보내기 전에, 미리 브라우저가 서버에게 교차 요청을 보내도 되는지 질의하는 단계다.

모든 교차출처요청에 Preflight를 보내진 않는다. 아래의 경우에만 preflight를 보내며, 캐싱도 된다.

  • 메서드가 GET, HEAD, POST 이 아니거나
  • 요청 헤더에 CORS에 자유롭지 않은 헤더가 포함되어있거나, 커스텀 헤더가 포함된 경우
    • Authorization 같은 표준 헤더도 safelisted가 아니다.
    • 교차출처에 자유로운 헤더들 : Accept, Accept-Language, Content-Language
  • Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, text/plain이 아니거나

정확한 Safelisted Header와 Safelisted Content-Type 목록 (preflight에 자유로운 친구들)

  • Content-Type: text/plain, multipart/form-data, application/x-www-form-urlencoded
  • Header : Accept, Accept-Language, Content-Language, Content-Type(위 값들에 한함)

굳이 왜 Preflight를 보낼까?

Preflight는 요청이 실제로 서버에 닿기 전에 ‘브라우저 단계에서’ 막기 위해 존재한다.

기본적인 CORS는 요청을 보낸 후, 응답만 안보여주는 것이라면 Preflight는 요청부터 차단한다. 특히 DB 조작이 일어나가나 어떤 헹동을 하게 만드는 API 요청 자체를 막기 위해 존재한다.


교차 Origin 간에 Cookie를 공유하기 위해 해야될 일

Access-Control-Allow-Credentials:true

우리가 api.a.com에서 받은 쿠키를 앞으로도 따라 보내게 하려면 Access-Control-Allow-Credentials=true 헤더가 필요하며 이때 우리는 fetch에 credentials include를 담아야한다 여기서 더 중요한 것은 교차출처 Origin이 와일드 카드면안된다.

응답 헤더에 Access-Control-Allow-Credentials: true 담아야만 교차오리진간 쿠키가 자동으로 딸려가는 것이 가능하다. 다만 Access-Control-Allow-Origin* 와일드카드일 때는 동작하지 않는다. 이 조건이 맞으면, 브라우저가 쿠키를 설정하게 된다.

또 여기서 조건이 하나가 더 있는데 fetch('/api',{credentials : 'include'}) 로 쿠키를 보낸다고 명시해야만 한다. 이래야만 요청 헤더에 Origin이 삽입된다.

물론 이것 외에도 Set-Cookie 디렉티브가 SameSite=Strict가 아니어야한다. 여기서 SameSite=Lax는 Get 요청에만 유효하므로 결국 SameSite=none을 추가해아하는데, 이 디렉티브 결국 Secure 디렉티브도 요구한다. 당연히 Secure가 존재하니 HTTPS여야만 하고, 인증서도 필요하다.


CORS는 브라우저가 응답을 JS에 노출하는 것을 제어할 뿐, 서버는 OPTIONS를 무시해도 실제 요청을 받으면 처리할 수 있다. 즉 보안은 서버가 반드시 자체적으로 검증해야 한다.


References

  • https://developer.mozilla.org/ko/docs/Glossary/Same-origin_policy